Einleitung


Collaborative Filtering ist ein bekannter Recommendation Algorithmus, der basierend auf den Bewertungen oder dem Verhalten von anderen Usern Empfehlungen macht. Ein Recommender kann für zwei Aufgaben eingesetzt werden. Einerseits kann eine personalisierte Liste mit empfohlenen Produkten für einen User zusammengestellt werden. Andererseits können spezifische Ratings für Produkte vorhergesagt werden. In dieser Mini-Challenge entwickeln und testen wir Recommender Systeme für Spielfilme.


Analyse des Datensatzes


Um gute Recommendersysteme entwickeln zu können, ist es wichtig, zuerst die Daten, auf denen die Recommender basieren sollen, zu analysieren und zu verstehen. Die Daten müssen üblicherweise auch noch einem Data Wrangling inklusive Data Cleansing unterzogen werden, damit die Algorithmen für die Recommender korrekt funktionieren.

sprintf('Anzahl User: %d', nrow(MovieLenseUser))
## [1] "Anzahl User: 943"
sprintf('Anzahl Filme: %d', nrow(MovieLenseMeta))
## [1] "Anzahl Filme: 1664"
sprintf('Anzahl Filme mit Ratings: %d', nrow(movies %>% semi_join(ratings, by = c('title' = 'item'))))
## [1] "Anzahl Filme mit Ratings: 1664"

Als Grundlage für diese Mini-Challenge steht der MovieLense-Datensatz mit 943 Usern und 1664 Filmen zur Verfügung, von welchen alle mindestens einmal bewertet wurden. Für einen Film ohne Bewertungen wäre es nicht möglich gewesen Ratings vorauszusagen, da als Basis dafür andere Ratings dieses Films dienen.

Die Filme sind jeweils einem oder mehreren Genres zugewiesen, wobei ein Genre mit dem Namen unknown existiert, welches auf fehlende oder fehlerhafte Daten hindeutet.

#Wie viele / Welche Filme haben genre "unknown"?
movies %>% 
    filter(unknown == 1)
movies <- movies[-which(movies$unknown == 1),]

Es sind zwei Filme diesem speziellen Genre zugeordnet, wobei bei einem auch der Name des Films unbekannt ist. Unter dem Film unknown könnten sich möglicherweise Ratings zu verschiedenen Filmen sammeln, bei welchen der Filmname unbekannt ist. Eine solche Zusammenfassung liefert keinen zusätzlichen Informationsgewinn und verzerrt im schlechtesten Fall den Recommender. Da wir nicht wissen, was es mit diesen zwei Filmen auf sich hat, entfernen wir sie aus dem Datensatz.


1. Welches sind die am häufigsten geschauten Genres/Filme?

Wir beantworten diese Frage mit der Häufigkeit der Bewertungen pro Genre/Film und gehen davon aus, dass Filme mit mehr Bewertungen auch öfters geschaut werden.

mwm <- most_watched_movies()
mwm
most_watched_movies_list(mwm)

Die 20 Filme mit den meisten Bewertungen scheinen Klassiker zu sein und haben jeweils hunderte Bewertungen. Der Film Star Wars wurde von 583 der 943 User bewertet, was über 60% sind.

Schauen wir uns die Verteilung der Ratings über alle Filme hinweg an.

most_watched_movies_hist(mwm)

Wir stellen wir fest, dass die allermeisten Filme sehr wenige Ratings haben - nur die allerwenigsten haben mehr als 100. Es gibt also einige sehr bekannte Filme und sehr viele, die eher unbekannt sind und daher von wenig Usern bewertet wurden. Betrachten wir nun die linke Seite der Grafik etwas genauer, also Filme mit sehr wenigen Bewertungen.

most_watched_movies_hist_few_ratings(mwm)

Wir sehen, dass 375 Filme 5 oder weniger Bewertungen erhalten haben, wobei bei 136 Filmen nur eine Bewertung vorhanden ist. Je weniger Bewertungen vorhanden sind, desto schwieriger ist es, Vorschläge für den Film zu machen.

Schauen wir uns die Verteilung der Ratings noch in einem Boxplot an.

sprintf('Median (Anzahl Ratings): %d', median(mwm$views))
## [1] "Median (Anzahl Ratings): 27"
most_watched_movies_boxplot(mwm)

Der Boxplot bestätigt die stark rechtsschiefe Verteilung. Über 75% der Filme haben weniger als 100 Bewertungen und der Median liegt bei lediglich 27 Bewertungen pro Film. Im Vergleich dazu sind Star Wars mit seinen 583 Bewertungen, wie auch die anderen Top 20 Filme, klare Ausreisser.


Nun interessieren wir uns für die Beliebtheit der Genres.

rpg <- ratings_per_genre()
rpg
most_watched_genre_list(rpg)

Diese ist sehr unterschiedlich, wie am obigen Barchart zu erkennen ist. Drama scheint mit grossem Abstand die beliebteste Kategorie zu sein. Am anderen Ende der Skala befinden sich Dokumentationen, welche einen verschwindend kleinen Anteil der Ratings ausmachen.

Wenn man die Anzahl Ratings pro Genre jedoch durch die Anzahl Filme in diesem Genre dividiert, ergibt sich ein leicht anderes Bild.

mpg <- movies_per_genre()
most_watched_gerne_rat_list(rpg, mpg)

Dann zeigt sich, dass Filme der Kategorie War am häufigsten bewertet werden und Drama auf den zweitletzten Platz zurückfällt. Dies zeigt, dass es grosse Unterschiede bei der Anzahl Filme pro Genre gibt und dass Filme der Genre War und Sci-Fi gut doppelt so oft bewertet werden wie Filme der Genres Drama, Horror, Comedy, Children’s oder Fantasy. Das Genre Documentary liegt auch hier wieder mit deutlichem Abstand am unteren Ende der Skala.


2. Wie verteilen sich die Kundenratings gesamthaft und nach Genres?

Die Filme im MovieLense Datensatz konnten von den Usern auf einer Skala von 1 bis 5 bewertet werden. Schauen wir uns die Verteilung der Ratings an.

ratings_distribution()

Der Datensatz enthält verhältnismässig wenige schlechte Beurteilungen, dafür viele im Mittelfeld. Dies ist insofern speziell, als dass davon ausgegangen wird, dass Menschen normalerweise Bewertungen schreiben, wenn ihnen das zu bewertende Objekt besonders oder überhaupt nicht gefällt.

Nun interessiert uns, ob dieses Bild über alle Genres hinweg zu finden ist.

ratings_distribution_per_genre()

Bei der Aufteilung der Bewertungen nach Genre ist die unterschiedliche Anzahl an Bewertungen pro Genre wieder gut erkennbar. Die Bewertungen sind aber bei allen Genres ähnlich verteilt, also relativ viele Bewertungen mit Werten 3 und 4 und jeweils wenige mit Werten 1 und 2.


3. Wie verteilen sich die mittleren Kundenratings pro Film?

Die mittlere Bewertung pro Film liegt zwischen 1 und 5 und auch bei der Standardabweichung gibt es grosse Unterschiede. Für 136 Filme konnte keine Standardabweichung berechnet werden bzw. beträgt sie 0, da lediglich eine Bewertung existiert.

show_mean_ratings_per_film()

show_std_ratings_per_film()

In den Scatterplots lässt sich erkennen, dass sich der Mittelwert mit Zunahme der Anzahl Ratings pro Film zwischen 3 und 4.5 einpendelt. Ab ca. 250 Bewertungen befinden sich die Mittelwerte nur noch in diesem Wertebereich, darunter streuen sie sehr stark. Filme mit mehr als 50 Bewertungen und einer mittleren Bewertung kleiner als 2 gibt es in diesem Datensatz nicht. Dies lässt sich dadurch erklären, dass aufgrund von irgendwelchen Empfehlungen (persönliche, Kritiken, …) gut bewertete Filme häufiger geschaut werden als unbeliebtere. Letztere erhalten somit auch keine neuen Bewertungen mehr. Die Standardabweichung konvergiert mit der Zunahme der Bewertungen nach 1, was wohl auf das Gesetz der grossen Zahlen zurückzuführen ist oder wiederum auf die Tatsache, dass in der Regel nur beliebte Filme weiterempfohlen werden.


4. Wie stark streuen die Ratings von individuellen Kunden?

Schauen wir uns nun die Streuung auf Kundenniveau und nicht mehr auf Filmniveau an.

plot_rating_distribution_per_customer(ratings)

Viele User bewerten Filme im Durchschnitt zwischen 3 und 4, wobei bei wenigen Ratings der Mittelwert auch höher oder niedriger liegen kann. Die Person, die mehr als 650 Filme und somit am meisten von allen bewertet hat, scheint eher kritisch zu sein und hat häufig negative Bewertungen vergeben. Die mittlere Bewertung dieser Person liegt unter 2. Ausserdem gibt es keine User im Datensatz, die ausschliesslich 1er oder nur 5er Bewertungen abgegeben haben, denn der tiefste Mittelwert eines Users liegt bei 1.5, der höchste bei 4.9.


5. Welchen Einfluss hat die Normierung der Ratings pro Kunde auf deren Verteilung?

Da nicht alle User gleich kritisch sind und die Bewertungsskala gleich interpretieren, kann es interessant sein, die Bewertungen zu normieren. Dies wird gemacht, indem von jeder Bewertung eines Users der Mittelwert über alle seine Bewertungen abgezogen wird. Eine neutrale Bewertung liegt somit für alle User bei 0. Schlechte Bewertungen haben dann einen negativen und gute einen positiven Wert. Ein weiterer Vorteil der Normierung ist, dass für fehlende Ratings der Wert 0 eingesetzt werden kann, ohne dass dies die Empfehlungen verzerrt. Wird das bei nicht normierten ratings gemacht, impliziert der Wert 0 bei einer fehlenden Bewertung immer eine negative Bewertung.

standardised_ratings <- as(rating_matrix - rowMeans(rating_matrix, na.rm = TRUE), 'realRatingMatrix')

plot_rating_distribution_per_customer(
  as(standardised_ratings, 'data.frame'), 
  'Verteilung der mittleren Ratings pro Kunde (normiert)',
  -1, 
  1
)

Im Scatterplot können wir auf einen Blick erkennen, dass der Mittelwert bei allen Kunden nach der Normierung genau 0 beträgt, hingegen bleibt die Standardabweichung bei allen Kunden gleich hoch wie vor der Normierung. Dies ist wie erwartet, denn die Differenz einer Bewertung zum Mittelwert beträgt immer noch gleich viel.


6. Welche strukturellen Charakteristika (z.B. Sparsity) und Auffälligkeiten zeigt die User-Item Matrix?

Um die User-Item Matrix noch besser zu verstehen, möchten wir einige Charakteristika mit Zahlen beschreiben.

sprintf('Kunden, die alle Filme bewertet haben: %d', sum(complete.cases(rating_matrix)))
## [1] "Kunden, die alle Filme bewertet haben: 0"
sprintf('Filme, die von allen Kunden bewertet wurden: %d', sum(complete.cases(t(rating_matrix))))
## [1] "Filme, die von allen Kunden bewertet wurden: 0"
sprintf('Durchschnittliche Anzahl Bewertungen pro Film: %d', round(mean(colCounts(MovieLense)), 0))
## [1] "Durchschnittliche Anzahl Bewertungen pro Film: 60"
sprintf('Durchschnittliche Anzahl Bewertungen pro User: %d', round(mean(rowCounts(MovieLense))))
## [1] "Durchschnittliche Anzahl Bewertungen pro User: 105"
sprintf('Sparsity: %f', calculate_sparsity(MovieLense))
## [1] "Sparsity: 0.936659"

Über 93% der Bewertungsmatrix enthält keine Werte. Das ist nicht erstaunlich, da es auch keine Kunden gibt, die alle Filme bewertet haben und es keine Filme gibt, die von allen Usern bewertet wurden. Der durchschnittliche User hat etwa 105 Bewertungen erstellt und jeder Film wurde durchschnittlich ca. 60 mal bewertet.

image(MovieLense)

Wenn wir die Matrix visualisieren, erkennen wir, dass eine bestimmte Struktur vorliegt. Es gibt eine Sortierung bei den Filmen und den Usern. Je weiter rechts, desto weniger Bewertungen hat ein Film. Dies bedeutet, dass wir die Items stets durchmischen müssen, bevor wir sie aufteilen. Ausserdem ist an den vertikalen Linien gut erkennbar, dass gewisse Filme viel häufiger bewertet wurden als andere.


Aufteilung des Datensatzes


Damit wir unsere Recommender später an zwei unterschiedlichen Datensätzen ausprobieren können, nehmen wir vom grossen MovieLense-Datensatz zwei unterschiedliche Stichproben. Diese sollen jeweils 700 Filme und 400 User enthalten. Filme mit sehr wenigen ratings möchten wir entfernen, damit die Matrix besser gefüllt ist.

set.seed(58976)
ratings_chantal <- generate_part()
ratings_joseph <- generate_part()

sprintf('[Chantal] Sparsity: %f, Anzahl Filme: %d, Anzahl User: %d', calculate_sparsity(ratings_chantal), ncol(ratings_chantal), nrow(ratings_chantal))
## [1] "[Chantal] Sparsity: 0.864243, Anzahl Filme: 700, Anzahl User: 400"
sprintf('[Joseph]  Sparsity: %f, Anzahl Filme: %d, Anzahl User: %d', calculate_sparsity(ratings_joseph), ncol(ratings_joseph), nrow(ratings_joseph))
## [1] "[Joseph]  Sparsity: 0.863871, Anzahl Filme: 700, Anzahl User: 400"

Unsere zwei Ratingsmatrizen nach der Aufteilung enthalten jeweils gleich viele Filme und Kunden. Die Matrizen sind mit ca. 13% Bewertungen gefüllt, der Rest ist jeweils leer. Dieser Wert ist deutlich besser als derjenige der gesamten Matrix, welcher bei ca. 7% liegt. Wir konnten die Sparsity also wie gewollt reduzieren.

image(ratings_chantal)

image(ratings_joseph)

Durch die Visualisierung der Matrizen ist gut erkennbar, dass beide einige der häufig bewerteten Filme zu enthalten scheinen und zudem auch Kunden, die häufig Filme bewertet haben. Ausserdem können wir keine bestimmte Struktur mehr erkennen, das heisst wir haben die Daten wirklich zufällig aufgeteilt.


Wir möchten zudem herausfinden, wie ähnlich die zwei so gefundenen Matrizen sind. Dazu nutzen wir das Mass “Intersection over Union” und berechnen also den Anteil an gleichen Filmen und Usern an allen ausgewählten Filmen und Usern. Dieser Wert liegt zwischen 0 und 1, je näher der Wert bei 1 ist, desto ähnlicher sind sich die Matrizen.

print('Ähnlichkeitsmass (IoU) für die Ratingmatrizen von Chantal und Joseph:')
## [1] "Ähnlichkeitsmass (IoU) für die Ratingmatrizen von Chantal und Joseph:"
print(round(compute_intersection_over_union(ratings_chantal, ratings_joseph),3))
## [1] 0.302

Wir sehen, dass etwas weniger als 30% der Matrizen übereinstimmt. Das bedeutet, dass sich unsere Datensätze zu einem grossen Teil unterscheiden. Somit können wir spätere Analysen vergleichen und können erkennen, ob die Resultate unabhängig vom spezifisch zugrunde liegenden Datensatz sind oder nicht.


Zuletzt interessiert uns noch, wie sich die mittleren Bewertungen der einzelnen Filme durch die Datenreduzierung verändert haben. Da nicht mehr zwingend zu jedem Film alle User und somit auch Ratings vorhanden sind, ist es möglich, dass sich die mittlere Bewertung eines Films verschoben hat.

compare_dataset_reduced()

In den zwei Scatterplots, die jeweils einen der zwei reduzierten Datensätze repräsentieren, sind auf der Diagnoalen die Filme zu erkennen, bei denen die Bewertung vor und nach der Reduzierung gleich geblieben ist. Punkte, die von diesen Diagonalen abweichen, haben mit der Reduzierung einen neuen Mittelwert erhalten. Bei beiden Datensätzen befinden sich die allermeisten Punkte auf der Diagonalen oder zumindest in der Nähe von dieser, und somit schliessen wir aus den Plots, dass die Abweichungen grösstenteils minimal sind. Es gibt vereinzelt grössere Abweichungen. Dies fällt aber in Anbetracht dessen, dass die reduzierten Datensätze 700 Filme enthalten, nicht stark ins Gewicht.


Ähnlichkeit


Ab hier basieren die Analysen jeweils auf einem reduzierten Datensatz. Im folgenden dient der Datensatz von Joseph als Grundlage.


In diesem Teil beschäftigen wir uns mit dem item-based Recommender. Stellen wir uns vor, wir wollen für User U vorhersagen, wie ihm Item i gefällt. Die Grundidee ist dann wie folgt: Wir suchen eine Gruppe von anderen Items, die über alle User hinweg ähnlich bewertet wurden wie Item i. Diese ähnlichen Items stellen die Neighborhood dar. Nun können wir schauen, wie User U die Items in der neighborhood bewertet hat und können somit vorhersagen, ob ihm Item i gefallen wird oder nicht.

Für einen item-based Recommender ist es also zentral, ähnliche Items zu finden. Dazu gibt es unterschiedliche Ähnlichkeitsmasse, die genutzt werden können. Wir gehen hier auf zwei ein, die Jaccard Similarity und die Cosine Similarity.

Bei der Jaccard Similarity wird geschaut, wie viele User die gleichen Items bewertet haben. Die Ähnlichkeit zweier Items berechnet sich also darüber, wie viele User beide Items bewertet haben, normalisiert mit der Anzahl an Usern insgesamt von denen die beiden Items bewertet wurden. Hier wird also nur geschaut, wie gross die Überschneidung der Bewertungen ist. Die Werte der Bewertungen werden ignoriert; ob es eine gute oder schlechte Bewertung war, wird nicht miteinbezogen. Dieses Ähnlichkeitsmass kann also für binäre ratings genutzt werden, beispielsweise wenn es darum geht, ob ein Artikel gekauft wurde oder nicht.

Die Cosine Similarity behebt genau dieses Defizit. Die Idee hier ist, dass wir für jeden Film einen Bewertungsvektor haben und somit den Winkel zwischen zwei Vektoren für die Ähnlichkeitsberechnung nutzen können. Im folgenden machen wir keine Normalisierung und setzen für fehlende Bewertungen den Wert 0 ein. Dies ist insofern problematisch, als dass 0 für eine schlechte Bewertung steht und somit fehlende Werte als eine negative Bewertung interpretiert werden. Dies könnte durch eine Normalisierung behoben werden. Damit beschäftigen wir uns hier aber nicht.


Wir möchten zuerst eigene Funktionen implementieren, die die oben beschriebenen Jaccard- und Cosine Similarity berechnen. Danach nutzen wir das Paket recommenderlab und erzeugen einen IBCF Recommender. Schliesslich analysieren wir die so erhaltene Ähnlichkeitsmatrix.

Die eigene Funktion soll so gebaut sein, dass sie als Input eine Bewertungsmatrix (User x Filme) nimmt und als Output eine symmetrische Ähnlichkeitsmatrix (Filme x Filme) ausgibt. In einer Zelle ij der Ähnlichkeitsmatrix kann dann jeweils die Ähnlichkeit von Film i mit Film j abgelesen werden. Damit die Berechnung effizient ist, nutzen wir Vektoroperationen. Wir schränken zudem die Bewertungsmatrix, die als Input gegeben wird, so ein, sodass nur 100 von den 700 Filmen miteinbezogen werden. Es entsteht also eine 100x100 Ähnlichkeitsmatrix. Die Werte der Ähnlichkeitsmatrix liegen alle zwischen 0 und 1. Grössere Werte bedeuten grössere Ähnlichkeit zwischen zwei Filmen.

Für die Jaccard Similarity brauchen wir binäre Ratings. Wir legen hier fest, dass Bewertungen die höher oder gleich 3 als gut zählen und somit eine 1 erhalten und tiefere Bewertungen eine 0.

chosen_ratings <- ratings_joseph
set.seed(9402)

# select 100 films randomly
ratings_sample <- chosen_ratings[,sample(ncol(chosen_ratings),100)]

jaccard_sim_mat(ratings_sample)
## [1] "Anzahl Nullen bei Jaccard: 2876"
print(paste("Übereinstimmung cosine eigene Implementierung mit recommenderlab:", all.equal(cosine_sim_mat(ratings_sample), cosine_sim_mat_reclab(ratings_sample))))
## [1] "Anzahl Nullen bei Cosine: 1294"
## [1] "Dauer eigene Implentierung von cosine: 0.012"
## [1] "Dauer recommenderlab cosine: 0.022"
## [1] "Übereinstimmung cosine eigene Implementierung mit recommenderlab: TRUE"

Vergleichen wir die Jaccard Ähnlichkeitsmatrix mit der cosine Ähnlichkeitsmatrix fällt auf, dass erstere mehr Einträge hat, die 0 sind. Das kommt daher, dass wir bei der Umwandlung in binary ratings alle Bewertungen kleiner als 3 gleich 0 setzen und somit werden sie gleich behandelt wie fehlende ratings und entsprechend mehr Ähnlichkeiten = 0.

Um etwas über die Effizienz der beiden Implementierungen zur Berechnung der cosine similarity aussagen zu können, stoppen wir die Zeit. Die ganze Vorbereitung, lassen wir weg und stoppen nur ab dem Zeitpunkt, ab dem die Matrix berechnet wird. Wir sehen, dass die eigene Implementierung etwa 4-8 mal schneller ist als diejenige von recommenderlab. Dass die vorimplementierte Funktion länger braucht, lässt sich damit erklären, dass recommenderlab noch viel mehr berechnet, als lediglich die Ähnlichkeitsmatrix.

Ein Vergleich der zwei cosine Ähnlichkeitsmatrizen mit der Funktion all.equal() zeigt, dass die selbst berechnete cosine Ähnlichkeitsmatrix identisch ist mit jener von recommenderlab.


IBCF Recommender

Nun sind wir nicht mehr nur an der Ähnlichkeitsmatrix interessiert, sondern am wirklichen IBCF Modell. Der Datensatz wird aufgeteilt in ein Trainings- und ein Testdatenset. Diese sollen im Verhältnis 4:1 stehen. Für eine solche Aufteilung gibt es verschiedene Möglichkeiten: split, bootstrap sample oder cross validation. Wir verwenden hier vorerst die einfachste Methode: split.

Dann trainieren wir ein IBCF Modell mit 30 Nachbarn und Cosine Similarity. Die Anzahl Nachbarn kann über den Parameter k festgelegt werden und bedeutet, dass aus der Ähnlichkeitsmatrix für jeden Film (also jede Zeile) die 30 höchsten Werte rausgesucht werden. Die Einstellungen für den IBCF recommender möchten wir so wählen, wie wir auch unsere selbst implementierte cosine Ähnlichkeit berechnet haben, daher setzen wir na_as_zero auf TRUE und normalize auf NULL. Schliesslich schauen wir uns an, welche Filme häufig für die Empfehlungen verwendet werden.

Der Parameter given bestimmt die Anzahl an Items, die von den 20 % Usern im Testdatensatz benutzt werden, um die Vorhersagen zu machen. Da wir Filme haben mit wenigen Bewertungen, sollte given nicht sehr hoch sein, da sonst keine Vorhersagen mehr evaluiert werden können. Wir setzen given auf 5. Für die Aufgabe hier ist es aber nicht relevant, da wir noch gar keine Vorhersagen machen.

#Zerlegung in Train and Test - passiert zufällig
set.seed(491)
data_split <- evaluationScheme(chosen_ratings, method='split', train=0.8, given=5)

#Item based recommender trainieren mit train daten
ibcf_recommender <- Recommender(getData(data_split, 'train'), method='IBCF', parameter=list(method='cosine', k=30, na_as_zero = TRUE, normalize= NULL))
ibcf_recommender
## Recommender of type 'IBCF' for 'realRatingMatrix' 
## learned using 320 users.

Die Ausgabe sagt uns, dass ein recommender auf einer Basis mit Daten von 320 Usern erstellt wurde. Die restlichen 80 User sind im Testdatensatz und wurden für das Lernen des Modells nicht benutzt. Somit haben wir das gewünschten Verhältnis von 4:1 erreicht.


Wie oben beschrieben, wurde die Nachbarschaft auf 30 eingegrenzt. Wir interessieren uns dafür, welche Filme für die Empfehlungen jeweils verwendet werden. Als erstes schauen wir uns an, ob die Nachbarschaften immer wieder aus denselben Filmen bestehen, also ob häufig die gleichen Filme als ähnlichste Filme klassifiziert werden.

sim_mat <- getModel(ibcf_recommender)$sim
plot_recommendation_frequency(sim_mat)

Die Verteilung zeigt uns, dass es einzelne wenige Filme gibt, die mehr als 50mal für eine Empfehlung herangezogen werden. Ein sehr grosser Anteil der Filme wird zwischen 0 und 50 mal in einer neighborhood zu finden sein. Es liegt also eine breite Verteilung vor.

Wir sehen hier ausserdem eine Liste mit den 10 am häufigsten als nächste Nachbarn klassifizierten Filme.

most_frequent_neighbours <- as.data.frame(head(sort(colCounts(as(sim_mat, 'realRatingMatrix')), decreasing = TRUE), 10))
names(most_frequent_neighbours) <- c('frequency')
most_frequent_neighbours

Schauen wir uns die Anzahl Bewertungen der ersten 5 Filme an, so fällt auf, dass dies sehr bekannte Filme sind. Wie wir in der nachfolgenden Liste sehen, so haben alle etwa 300 Bewertungen. Spannend ist der grosse Einfluss des Parameters normalize. Wenn wir für die Berechnung der Ähnlichkeiten die Bewertungen normalisieren, wären auf dieser Rangliste ganz andere, viel weniger bekannte Filme zu finden. Das hat uns überrascht, da wir sogenannte Klassiker als Filme erwarten, die sich gut vergleichen lassen. Wir können uns diesen grossen Einfluss der Einstellung normalize nicht erklären.

chosen_ratings_df <- as(chosen_ratings, 'data.frame')
#chosen_ratings_df[item == 'Star Wars (1977)']

sprintf('Star Wars (1977): %d', nrow(filter(chosen_ratings_df, item=='Star Wars (1977)')))
## [1] "Star Wars (1977): 355"
sprintf('Raiders of the Lost Ark (1981): %d', nrow(filter(chosen_ratings_df, item=='Raiders of the Lost Ark (1981)')))
## [1] "Raiders of the Lost Ark (1981): 328"
sprintf('Return of the Jedi (1983): %d', nrow(filter(chosen_ratings_df, item=='Return of the Jedi (1983)')))
## [1] "Return of the Jedi (1983): 325"
sprintf('Empire Strikes Back, The (1980): %d', nrow(filter(chosen_ratings_df, item=='Empire Strikes Back, The (1980)')))
## [1] "Empire Strikes Back, The (1980): 289"
sprintf('Pulp Fiction (1994): %d', nrow(filter(chosen_ratings_df, item=='Pulp Fiction (1994)')))
## [1] "Pulp Fiction (1994): 293"

Vergleich zwischen Datensets

Die zehn häufigsten verwendeten Filme in der Ähnlichkeitsmatrix sind bei den beiden reduzierten Datensets sehr ähnlich. In den Top-5 kommen die gleichen Filme vor, wobei Return of the Jedi und Raiders of the lost Ark den Platz gewechselt haben. Im unteren Teil der Liste sind jeweils 3 Filme zu finden, die es beim anderen Datenset nicht in die Top-10 geschafft haben. Die Anzahl der Vorkommnisse unterscheiden sich minimal. Ebenfalls kleine Abweichungen gibt es bei der Anzahl Bewertungen dieser zehn Filme. Dies zeigt, dass unterschiedliche User für diese Filme in die reduzierten Datensets übernommen wurden. Bei der Datenreduzierung wurden die Filme zufällig ausgewählt, demnach wäre es auch möglich gewesen, dass einer dieser Top-Filme in einem Datenset nicht vorhanden ist. Wir stellen aber fest, dass dies nicht der Fall ist und die Recommender auf ähnlichen Filmen basieren.


Top-N Listen Vergleich IBCF, UBCF und SVD


Nun wollen wir erste Top-N Listen mit Filmvorschlägen generieren lassen. Einerseits verwenden wir wieder ein item-based Modell, andererseits ein user-based Modell. Dieses sucht nicht ähnliche Items, sondern ähnliche User, um die neighborhood zu bilden. Die fehlende Bewertung für einen Film wird dann aus den Bewertungen der ähnlichen User für den gefragten Film berechnet.

Dem Recommender wird ein oder mehrere User übergeben für die er alle fehlenden Filmbewertungen berechnet und dann die Filme zurückgibt, für welche er die höchsten Bewertungen berechnet hat. Filme, die der Benutzer bereits selbst bewertet hat, werden nicht in die Top-N Liste aufgenommen. Bei diesen Beispielen teilen wir den Datensatz nicht in einen Trainings- und Testdatensatz auf, da wir nur die generierten Top-N Listen betrachten, diese aber nicht mit dem Datensatz validieren werden.

Wir erstellen nun also einen IBCF- und einen UBCF-Recommender, die beide die Cosine Similarity verwenden und dabei 30 Nachbarn berücksichtigen. Beim IBCF-Recommender bedeuten die 30 Nachbarn 30 ähnliche Filme und beim UBCF folglich 30 ähnliche User. Für die anderen Parameter werden die Standardwerte von recommenderlab übernommen.

set.seed(67)
rm(ibcf_recommender)
ibcf_recommender <- Recommender(chosen_ratings, method='IBCF', parameter=list(method='cosine', k=30))
ubcf_recommender <- Recommender(chosen_ratings, method='UBCF', parameter=list(method='cosine', nn=30))
compare_ratings_recommender(ibcf_recommender, ubcf_recommender, chosen_ratings)

Für einen zufälligen Testkunden haben wir nun die Top 15-Vorschläge einmal mittels IBCF- und einmal mittels UBCF-Recommender generiert und nebeneinander dargestellt. Auffällig sind die berechneten Ratings des IBCF, welche alle 5.0 für diesen Testkunden betragen. Wenn man jedoch die Liste für diesen Kunden verlängern würde, hat lediglich Platz 16 ebenfalls noch die Bewertung 5.0. Bei Items weiter unten in der Liste sinken die Ratings auf 4.5, etc. Es scheint also nicht so zu sein, dass beim IBCF dutzende Filme dieselbe Bewertung erhalten und die Top 15 nur eine zufällige Auswahl von Filmen mit gleichen Bewertungen ist.

Eine weitere Auffälligkeit sind die UBCF-Ratings, welche das Skalamaximum von 5.0 teilweise deutlich übersteigen. Der Grund dafür liegt bei der aktivierten Normalisierung, welche die Bewertungen für jeden User um den Wert 0.0 verteilt indem dessen Durchschnittsbewertung bei jeder seiner Bewertungen abgezogen wird. Dadurch werden fehlende Bewertungen, welche für die Cosine-Similarity auf 0 gesetzt werden, nicht als negative sondern als neutrale Bewertungen gewichtet. Zudem werden dadurch die unterschiedlichen Bewertungsverhalten der User ausgeglichen, so dass auch kritischere User in die Nachbarschaft eines Users mit vielen 5-Bewertungen aufgenommen werden. Voraussetzung ist natürlich, dass die Ähnlichkeit zwischen den Usern gegeben ist. Bei der Berechnung der fehlenden Bewertungen anhand der Bewertungen der User in der Nachbarschaft, werden die Bewertungen wieder denormalisiert, damit sie mit den bestehenden Bewertungen des Users vergleichbar sind. Dies kann dazu führen, dass Bewertungen ausserhalb der Skala entstehen, wenn beispielsweise ein kritischer User einen Film für ihn aussergewöhnlich hoch bewertet hat und diese Bewertung für einen User mit einer bereits hohen Durchschnittsbewertung verwendet wird.


Theoretisch wäre es deswegen auch möglich, dass Bewertungen unterhalb 1.0 generiert werden. Dazu müssten jedoch normalisierte Bewertungen unter 0 in der Nachbarschaft vorhanden sein. Solche berechneten Bewertungen werden aber kaum in einer Top N-Liste angezeigt werden, da darin nur die Filme mit den höchsten Bewertungen aufgelistet werden. Und tatsächlich, bei keinem Kunden befindet sich in dessen Top 15-Liste eine Bewertung kleiner als 1:

rating_st_1(ubcf_recommender, chosen_ratings)
## [1] 0

Wenn jedoch alle berechneten Bewertungen für alle Kunden abgerufen werden, zeigen sich einige Bewertungen unterhalb der Skala. Insgesamt sind es 2375, die kleiner als 1 sind, wobei die niedrigste Bewertung bei fast -0.95 liegt.

below_1_ratings <- all_ubcf_st_1(ubcf_recommender, chosen_ratings)

sprintf('Anzahl Ratings < 1.0: %d', nrow(below_1_ratings))
## [1] "Anzahl Ratings < 1.0: 2375"
head(below_1_ratings, n = 10)
plot_all_ubcf_st_1(below_1_ratings)


Und die letzte Erkenntnis dieser Auswertung der Top 15-Listen einzelner Kunden ist, dass es bei diesem Beispielkunden keine einzige Überschneidungen zwischen den Filmen der beiden Listen gibt. Unsere Erwartung an die beiden Recommender war eigentlich, dass es grössere Überschneidungen bei den Top 15 Vorschlägen zwischen den verschiedenen Modellen gibt. Wir wollen nun überprüfen, ob dies ein Einzelfall ist, oder ob es diese grossen Unterschiede bei vielen Kunden gibt.

top_n_ibcf <- predict(ibcf_recommender, chosen_ratings, n=15)
top_n_ubcf <- predict(ubcf_recommender, chosen_ratings, n=15)
intersection_recommender(top_n_ibcf, top_n_ubcf) + labs(title = 'Überschneidungen Top-15 IBCF und UBCF mit ordinalen Ratings')

Bei den insgesamt 400 Kunden im reduzierten Datensatz gab es in den Top 15-Empfehlungen bei lediglich einem Kunden oder einer Kundin eine Überschneidung von drei Filmen. Höhere Überschneidungen zwischen IBCF- und UBCF-Listen sind nicht vorhanden. Dafür ergibt sich bei über 77% der Kunden keine einzige Überschneidung in den Empfehlungen. Somit scheint dies kein Einzelfall gewesen zu sein, sondern die allermeisten Kunden zu betreffen.


Schauen wir uns an, wie die Verteilung der Filme in den Top-15-Listen der unterschiedlichen Recommender ist.

ibcf_occurrences <- count_topN_occurrences(top_n_ibcf, chosen_ratings)
ubcf_occurrences <- count_topN_occurrences(top_n_ubcf, chosen_ratings)

plot_topN_distribution(ibcf_occurrences) + labs(title = 'Verteilung Top 15 Filme des IBCF Recommenders')

plot_topN_distribution(ubcf_occurrences) + labs(title = 'Verteilung Top 15 Filme des UBCF Recommenders')

Wenn wir die Häufigkeit der einzelnen Filme in den Vorschlägen in einem Histogramm darstellen, zeigt sich bei beiden Recommendern eine rechtsschiefe Verteilung. Es fällt auf, dass das IBCF-Modell viel diverser empfiehlt als das UBCF-Modell. Bei letzterem erscheinen ca. 250 der 700 Filme gar nicht in den Vorschlägen, stattdessen gibt es einige Filme, die über 90 mal in den Vorschlägen vorkommen.

top_recommended_movies_df <- top_recommended_movies(ibcf_occurrences, ubcf_occurrences)
top_recommended_movies_df
plot_top_recommended_movies(top_recommended_movies_df)

Laura wird gar 107 mal empfohlen. Beim item-based Recommender heisst der meist empfohlene Film A Chef in Love und ist lediglich 91 mal in den Listen vorhanden. Dafür fehlen auch nur ca. 94 Filme in den Vorschlägen, die restlichen ca. 606 Filme werden empfohlen.

Wenn man für jeden Film die Häufigkeiten gegenüberstellt, wie oft er in den IBCF- und UBCF-Vorschlägen auftaucht, lässt sich keine Korrelation erkennen.

corr_rec_ibcf_ubcf(ibcf_occurrences, ubcf_occurrences)

Im Gegenteil, der Scatterplot zeigt, dass es viele Filme gibt, die im item-based Verfahren vorgeschlagen werden und im user-based Verfahren nicht vorkommen und dies teilweise auch umgekehrt. In beiden Fällen ist es sogar so, dass häufige Resultate des einen Recommenders gar nicht oder nur selten vom anderen Recommender vorgeschlagen werden. Die empfohlenen Filme dieser beiden Modelle sind also sehr unterschiedlich.


Nachdem wir IBCF und UBCF mit ordinalem rating und Cosine similarity verglichen haben, schauen wir uns nun an, wie die Übereinstimmung bei Recommendern mit binären ratings und Jaccard similarity aussieht. Die Erwartung ist allerdings, dass wir hier noch weniger Übereinstimmung finden, da viele Informationen verloren gehen, wenn wir die ratings in binäre ratings umwandeln und der Recommender somit schlechter wird.

chosen_ratings_bin <- binarize(chosen_ratings, minRating=3)
ibcf_recommender_bin <- Recommender(chosen_ratings_bin, method='IBCF', parameter=list(method='jaccard', k=30))
ubcf_recommender_bin <- Recommender(chosen_ratings_bin, method='UBCF', parameter=list(method='jaccard', nn=30))
top_n_ibcf_bin <- predict(ibcf_recommender_bin, chosen_ratings_bin, n=15)
top_n_ubcf_bin <- predict(ubcf_recommender_bin, chosen_ratings_bin, n=15)

intersection_recommender(top_n_ibcf_bin, top_n_ubcf_bin) + labs(title = 'Überschneidungen Top-15 IBCF und UBCF mit binären Ratings')

Wie erwartet haben wir hier eine noch kleinere Übereinstimmung. Bei lediglich 4 der 400 Usern wurde eine einzige Übereinstimmung in den Top 15 Listen gefunden. Bei allen anderen gar keine.

Vergleichen wir nun noch die Top 15 Listen des UBCF mit Cosine Similarity und mit Jaccard Similarity. Da hier das Prinzip bei beiden user-based ist, wird eine grössere Übereinstimmung erwartet.

intersection_recommender(top_n_ubcf, top_n_ubcf_bin) + labs(title = 'Überschneidungen Top-15 UBCF mit ordinalen und binären Ratings')

Wie erwartet ist die mittlere Anzahl Überschneidungen mit 0.38 ein bisschen grösser als beim Vergleich von IBCF und UBCF, wo sie bei 0.26 lag. Allerdings gibt es auch hier nur bei drei Personen eine Überschneidung von 3 Empfehlungen zwischen den Top-N-Listen von binären und ordinalen ratings. Bei 14 Personen treten 2 Empfehlungen in beiden Listen auf und bei 116 Usern gibt es genau eine Übereinstimmung zwischen den beiden Listen.


Nun möchten wir noch den SVD Recommender dazu nehmen und ebenfalls Vergleiche aufstellen. SVD bedeutet singular value decomposition. Der Recommender funktioniert so, dass im Hintergrund Kategorien generiert werden und schliesslich Vorhersagen gemacht werden können, indem geschaut wird, wie stark sich ein User für die bestimmte Kategorie interessiert und wie relevant das Item in dieser Kategorie ist. Um für die ratings matrix eine Singulärwertzerlegung zu machen, müssen fehlende Werte eingesetzt werden. Gemäss Ekstrand (2011) hat sich gezeigt, dass es eine gute Imputationsstrategie ist, die fehlenden Werte durch die mittlere Bewertung eines Items zu ersetzen. Das wird im folgenden gemacht. Mit dem Parameter k können wir die Anzahl Kategorien bzw. Singulärwerte bestimmen, die gebildet werden sollen. Wir vergleichen die Top 15 Liste von IBCF und SVD mit 10, 20, 30, 40 und 50 Singulärwerten.

comp_ibcf_svd(chosen_ratings)

Wir erkennen hier im Mittel etwa halb so viele Übereinstimmungen wie zwischen UBCF und IBCF. Je nach Datensatz führt die Anzahl an Singulärwerten zu einer grösseren oder kleineren Übereinstimmung. Bei unseren zwei Datensätzen ist die höchste Übereinstimmung zwar jeweils bei k = 50. Aber das Datenset von Chantal zeigt, dass k = 20 oder 30 ähnliche Werte erzielt, was bei Joseph nicht erkennbar ist. Das heisst, es muss je nach Datensatz individuell entschieden werden, wie viele Singulärwerte sinnvoll sind.


Vergleich zwischen Datensets

Abgesehen von den unterschiedlichen Werten beim SVD-Modell, sind bei den Auswertungen meist nur kleine Unterschiede zwischen den Datensets erkennbar. Auffällig sind hingegen noch die Unterschiede bei den Filmen, welche am häufigsten empfohlen werden. Über die beiden Top 10-Listen hinweg gibt es bei den UBCF-Recommendern zwei Übereinstimmungen zwischen den Datensets, bei den IBCF-Recommendern drei. Interessanterweise befinden sich zwei davon in beiden Datensets auf denselben Plätzen, nämlich auf den Plätzen 2 und 6. Trotzdem stimmt die Anzahl Empfehlungen nicht überein und deshalb dürfte dieselbe Platzierung zufällig sein.

Beim genaueren Untersuchen der vier Filme, die jeweils den ersten Platz besetzen, fällt auf, dass lediglich Laura in beiden Datensets vorhanden ist und auch im Verhältnis zu den anderen sehr viele Bewertungen erhalten hat. Die anderen drei Filme sind jeweils nur in einem Datensatz zu finden und enthalten auch sehr wenige Bewertungen, was ziemlich erstaunlich ist. Wahrscheinlich dienen diese wenigen Kund*innen, die diese Filme bewertet haben, bei vielen anderen als ähnliche Kund*innen, was die grosse Menge an Empfehlungen erklären würde. Beziehungsweise beim IBCF-Modell korrelieren diese Filme wahrscheinlich gut mit anderen Filmen.

Die Filme haben bis auf Captives auch relativ hohe Durchschnittsbewertungen. Bei Captives betragen die zwei Bewertungen 1 und 4. Entweder ist hier nur die höhere Bewertung bei der Berechnung der Bewertungen ausschlaggebend gewesen oder es wurden auch antikorrelierende Werte beachtet und entsprechend eingerechnet. Ansonsten würden für diesen Film nicht solche hohen Bewertungen generiert, welche ihn so oft in die Top 15-Listen bringen.

get_top_recommended_movies_stats(ratings_chantal, c('Laura (1944)', 'Aparajito (1956)', 'A Chef in Love (1996)', 'Captives (1994)'))
get_top_recommended_movies_stats(ratings_joseph, c('Laura (1944)', 'Aparajito (1956)', 'A Chef in Love (1996)', 'Captives (1994)'))

Top-N Listen Metrik und Monitor


Metriken für eine Beurteilung der Empfehlungen sind Precision, Recall, Coverage und Novelty. Wir interessieren uns hier für letztere zwei. Die Coverage sagt aus, wie viele Filme aus der ganzen Palette empfohlen werden. Die Novelty ist ein Mass dafür, wie viele Filme, die gemäss ratings eher weniger populär sind, empfohlen werden. Die zwei Masse zusammen sagen etwas darüber aus, wie individuell die Empfehlungen des Recommenders sind. Sind Novelty und Covearge klein, so werden immer die gleichen paar populären Filme empfohlen.

ibcf_recommender <- Recommender(chosen_ratings, method='IBCF')
top_n_ibcf <- predict(ibcf_recommender, chosen_ratings, n=15)
paste('Coverage:', round(compute_coverage(as(top_n_ibcf, 'list')), 1))
## [1] "Coverage: 86.4"
paste('Novelty:', round(compute_novelty(as(top_n_ibcf, 'list'), chosen_ratings), 1))
## [1] "Novelty: 5.9"

Der IBCF Recommender (default Einstellungen) hat eine Coverage von 86.4 %. Das entspricht dem, was wir bereits weiter oben festgestellt haben, als wir festgehalten haben, dass etwa 610 von 700 Filmen empfohlen werden. Die Novelty liegt bei 5.9 - dieser Wert ist schwieriger zu interpretieren. Eine Novelty von 0 bedeutet, dass die Filme, die empfohlen werden, von allen Usern bewertet wurden. Eine Novelty von 1 bedeutet, dass die Filme, die empfohlen werden, von der Hälfte der User bewertet wurden. Je grösser die Zahl, desto grösser die Novelty. Wenn wir später unterschiedliche Recommender verlgeichen, werden wir feststellen, dass 5.9 ziemlich hoch ist.


Im Folgenden möchten wir die Qualität von unterschiedlichen Recommendern überprüfen, indem wir uns die Genres anschauen. Uns interessiert, welches Genre einem Kunden wie häufig empfohlen wird und wie häufig der Kunde in der Vergangenheit Filme aus diesem Genre gut bewertet hat. Die blauen Punkte zeigen, wie viel Prozent der empfohlenen Filme auf der Top-N-Liste aus einem bestimmten Genre stammen. Die roten Punkte zeigen, wie viele von den gut bewerteten Filmen in dieses Genre gehören. Gute Bewertung bedeutet hier, der User hat sie mit 4 oder 5 bewertet.

Um unterschiedliche Recommender miteinander vergleichen zu können, haben wir eine Qualitätsmetrik definiert. Wir berechnen dazu jeweils die Summe der Differenzen zwischen den roten und den blauen Punken für 20 zufällige User. Anhand dieser Zahl vergleichen wir anschliessend unterschiedliche Recommender.

set.seed(5)

# sample of 20 users
samp <- sample(chosen_ratings, 20)

ibcf_recommender <- Recommender(chosen_ratings, method='IBCF', parameter=list(method='cosine', k=50))
cleaveland_plot(ibcf_recommender, samp)
## [1] "Methode: IBCF cosine, Summe der Differenzen: 1602"

ibcf_recommender <- Recommender(chosen_ratings, method='IBCF', parameter=list(method='pearson', k=30))
cleaveland_plot(ibcf_recommender, samp)
## [1] "Methode: IBCF pearson, Summe der Differenzen: 1632"

ubcf_recommender <- Recommender(chosen_ratings, method='UBCF', parameter=list(method='cosine', nn=50))
cleaveland_plot(ubcf_recommender, samp)
## [1] "Methode: UBCF cosine, Summe der Differenzen: 1669"

ubcf_recommender <- Recommender(chosen_ratings, method='UBCF', parameter=list(method='pearson', nn=10))
cleaveland_plot(ubcf_recommender, samp)
## [1] "Methode: UBCF pearson, Summe der Differenzen: 1517"

svd_recommender <- Recommender(chosen_ratings, method='SVD', parameter=list(k=10))
cleaveland_plot(svd_recommender, samp)
## [1] "Methode: SVD, Summe der Differenzen: 1248"

Wir können erkennen, dass bezüglich dieser Metrik der SVD Recommender mit 10 Singulärwerten am besten abschneidet. Die Anzahl Singulärwerte hat allerdings keinen grossen Einfluss. Der SVD-Recommender hat allgemein die tiefsten Differenzen und die anderen Modelle sind dann deutlich schlechter.

In den Grafiken für jeweils einen Test-User ist zu erkennen, dass die Häufigkeit der Empfehlung eines Genres teilweise um mehr als 40 Prozentpunkte davon abweicht, wie häufig der User solche Filme positiv bewertet hat.


Vergleich zwischen Datensets

Das IBCF-Modell von Chantal erzielt eine leicht höhere Coverage, wobei dasjenige von Joseph eine minimal höhere Novelty erreicht. Beide Werte sind bei den beiden Modellen dennoch sehr gut und die Differenzen vernachlässigbar. Die unterschiedlichen Modelle zeigen auch bei den Genre-Vergleichen ähnliche Differenzwerte bei beiden Datensets. Die besten Resultate werden jeweils mit SVD erzielt und die Cosine-Modelle schnitten jeweils am schlechtesten ab. Die Abweichungen zwischen den Datensets sind relativ gering und es weisen über die Datensets hinweg jeweils ähnliche Genres wie Drama, Comedy und Action grössere Unterschiede auf.


Wahl des optimalen Recommenders


Abschliessend wollen wir nun den optimalen Recommender für den reduzierten MovieLense-Datensatz bestimmen. Dazu werden wir unterschiedliche Modelle erstellen und diese anhand einer Performancemetrik bewerten. Diese Metrik erstellen wir aus Sicht eines Streaminganbieters, der seinen Nutzer*innen möglichst interessante Filme empfehlen möchte, damit diese mehr Zeit auf seiner Plattform verbringen. Die Plattform zeigt jedem Benutzer und jeder Benutzerin eine Top N-Liste mit Empfehlungen an, von welchen möglichst viele gefallen sollen. Das Verhältnis der Menge an Items, die gefallen sollen, zur Anzahl Empfehlungen wird durch die Precision ausgedrückt, die nun für den folgenden Vergleich als Performancemetrik verwendet wird. Der Recall-Wert, welcher dem Verhältnis der empfohlenen positiv empfundenen Items zur Gesamtzahl der positiv empfundenen Items entspricht, ist für diesen Anwendungsfall nicht relevant, da es ausreicht, wenn möglichst viele Items in den Listen vorhanden sind, die gefallen.

Bei den Metriken, die für die Plattform wichtig sein könnten, fällt die Coverage weg, da ein Streaminganbieter nicht darauf angewiesen ist, möglichst unterschiedliche Filme zu empfehlen. Ein physisches Lager muss von ihm nicht bewirtschaftet werden. Hingegen sollte der Novelty-Wert beachtet werden, damit den Kund*innen nicht nur Blockbusters empfohlen werden. Diese können auch in einer separaten Most-Watched-Liste mit denselben Filmen für alle Kund*innen dargestellt werden. Da die von Recommender-Systemen generierten Top N-Listen möglichst personalisiert sein sollen, sollte ein Recommender mit einem hohen Novelty-Wert gewählt werden.

Messwerte, die die Präzision der berechneten Ratings (MSE, RMSE, etc.) ausdrücken, sind ebenfalls weniger interessant, da lediglich die Top N-Listen relevant sind. Dort ist wichtig, dass die vorgeschlagenen Filme für die Kund*innen interessant sind und weniger, ob sie von den Kund*innen ebenfalls so bewertet werden würden.


Vorbereitung Metrikberechnung

Wir werden die evaluate-Funktion von recommenderlab verwenden, die einige Kennzahlen zu den zusammengestellten Top N-Listen liefern kann. Unter anderem wird die Precision und der Recall-Wert berechnet. Damit bei diesen Kennzahlen auch der Novelty-Wert berechnet wird, überschreiben wir die calcPredictionAccuracy-Funktion, die von evaluate verwendet wird, mit einer von uns angepassten Version. Als Grundlage dient der aktuelle Code aus Github, in den wir unsere Funktionen compute_coverage und compute_novelty integrieren.

overwrite_calc_prediction_accuracy()

Training

Wir wollen fünf unterschiedliche Modelle trainieren und diese anschliessend miteinander vergleichen. Zudem werden wir auch einen Top-Movie- und einen Random-Recommender als Referenz für den Vergleich erstellen. Für die Recommender werden wir dieselben Parameterwerte verwenden, die wir bereits bei den vorherigen Analysen eingesetzt haben. Nach der Wahl des optimalen Modells werden wir dessen Parameter noch optimieren. Für die Evaluation wird jedes Modell mit 80% der Daten trainiert und anschliessend für alle User Top N-Listen mit den Längen 10, 15, 20, 25 und 30 generiert. Für diese Listen werden mit der vorher erwähnten evaluate-Funktion und den restlichen 20% der Daten die Kennzahlen berechnet. Die Daten werden dabei mit zehnfacher Kreuzvalidierung aufgeteilt und überprüft.

Aus den ordinalen Ratings werden wir einen user-based, einen item-based und einen SVD-Modell-Recommender erstellen. Zudem werden wir die ordinalen Ratings wieder in binäre umrechnen und jeweils einen item-based und einen user-based Recommender mit der Jaccard-Ähnlichkeit erstellen. Als positives Rating wählen wir Bewertungen grösser oder gleich 3. Diesen Schwellwert wählen wir auch für die Berechnung der Werte TP, FP, etc.


Evaluation

Bemerkung: Es kann sein, dass die folgende Funktion fehlschlägt, dann bitte einfach nochmals laufen lassen.

set.seed(5)
scheme_ordinal <- evaluationScheme(chosen_ratings, method="cross", k=10, train=0.8, given=3, goodRating=3)
binary_ratings <- binarize(chosen_ratings, minRating=3)
scheme_binary <- evaluationScheme(binary_ratings, method="cross", k=10, train=0.8, given=3, goodRating=3)

plot_metrics(c(avg(metrics_ordinal_recommenders(scheme_ordinal)), avg(metrics_binary_recommenders(scheme_binary))))

Im Diagramm oben sind die Precision- und Novelty-Werte der sieben Recommender-Modelle dargestellt. Die Werte werden jeweils für jede Listenlänge angegeben. Wie erwartet erreicht der Popular-Recommender die besten Precision- und die schlechtesten Novelty-Werte, da er möglichst beim Gesamtpublikum beliebte Filme vorschlägt. In der anderen Ecke befindet sich das IBCF-Modell mit der Cosine-Similarity, für welches bei allen Listenlängen sehr schlechte Precision-Werte erzielt wurden. Dafür ist die Novelty aussergewöhnlich hoch. Diese Precision ist jedoch unbrauchbar, da sogar zufällige Vorschläge bessere Werte diesbezüglich erreicht haben. Die Ergebnisse beider UBCF-Modelle entsprechen ungefähr denjenigen des Zufallsmodells und liegen deshalb ebenfalls unter unseren Erwartungen. Wir sind mit der Annahme an diese Aufgabe gegangen, dass alle untersuchten Modelle bessere Ergebnisse als reine Zufallsresultate liefern. Auch eine andere Parameterisierung oder ein IBCF-Modell mit der Pearson-Ähnlichkeit liefern ähnlich schlechte Werte. Wir schliessen daraus, dass abhängig vom Datensatz Recommenderssysteme auch sehr schlechte Resultate liefern können.

Bezüglich Precision sind die Kennzahlen des SVD-Modells und des item-based Recommenders mit der Jaccard-Ähnlichkeit erfreulich. Letzterer erreicht sogar annähernd die Werte des Popular-Modells, was jedoch auch wieder auf Kosten der Novelty geht. Das SVD-Modell hingegen erreicht bei dieser Metrik bessere Werte und hat dennoch eine akzeptable Precision erzielt. Grundsätzlich scheint es so, dass zumindest bei diesem Datensatz nicht bei beiden Metriken hohe Werte erzielt werden können. Die Resultate liegen auch alle ungefähr auf einer Kurve, welche von oben links nach unten rechts verläuft. Somit muss bei der Wahl ein guter Kompromiss gefunden werden. Das SVD-Modell scheint ziemlich ausgeglichen, was die beiden Metriken anbelangt. Ungefähr ein Drittel der Filme in den Top N-Listen scheinen gute Resultate zu sein und es werden auch nicht allzu bekannte Filme empfohlen. Die Precision-Werte sind aus unserer Sicht bei Popular und IBCF mit der Jaccard-Ähnlichkeit nicht ausreichend gut, als dass sie die schlechten Novelty-Werte ausgleichen würden. Und da die anderen Recommender nicht besser als ein Random-Recommender sind, schlagen wir ein SVD-Modell für diesen Datensatz als optimalen Recommender vor. Ein weiterer Grund, das SVD-Modell zu wählen, waren die vorherigen Analysen zu den Genres. Bei diesen hat ebenfalls SVD am besten abgeschnitten.


Hyperparametertuning

Nachdem wir nun das Modell des optimalen Recommenders festgelegt haben, wollen wir dessen Parameter optimieren, um die Resultate des Recommenders noch weiter zu verbessern. Einerseits kann die Normalisierung der Ratings aktiviert oder deaktiviert werden, andererseits lässt sich die Kompressionsstufe mit dem Parameter k definieren, welcher somit die Grösse der Matrizen in der Berechnung beeinflusst. Je kleiner dieser Wert ist, desto weniger Informationen können aus der User/Rating-Matrix übernommen werden und desto grösser werden die Überschneidungen zwischen den einzelnen Usern und Filmen. Zudem wäre es möglich den Parameter maxiter zu erhöhen. Der Standardwert von 100 ist jedoch ausreichend für gute Resultate. Bei einer testweisen Erhöhung auf 500, konnten keine signifikant besseren Werte erzielt werden.

Wir wollen nun SVD-Modelle trainieren und für den Parameter k die Werte 10, 20, 30, 40 und 50 einsetzen und je einmal die Normalisierung aktivieren und deaktivieren.

plot_metrics_svd(scheme_ordinal)

Durch das Verändern der Parameter verändern sich auch die Precision- und Novelty-Werte. Auch hier lässt sich schnell erkennen, dass bei Modellen mit einer hohen Precision die Novelty darunter leidet und umgekehrt. Auch hier liegen die Punkte auf einer Geraden beziehungsweise einer leicht gekrümmten Kurve. Interessanterweise schneidet jeweils das Modell ohne Normalisierung bzgl. Precision besser ab als das Modell mit dem gleichen k-Wert und mit aktivierter Normalisierung. Zudem scheinen sich niedrige k-Werte positiv auf die Precision auszuwirken. Beide Modelle mit k = 10 besitzen die besten Precision. Aus diesen Gründen wählen wir das SVD-Modell mit k = 10 und ohne aktivierter Normalisierung als optimalen Recommender für unseren gewählten Anwendungsfall und diesen Datensatz.


Vergleich zwischen Datensets

Chantals Modelle erzielen, wie bereits beim IBCF-Modell bemerkt, leicht bessere Novelty-Werte, dafür haben die Recommender-Systeme von Joseph eine leicht höhere Precision. Dies bestätigt wiederum den Eindruck, dass Verbesserungen der Novelty auf Kosten der Precision gehen. So erreicht Josephs bestes SVD-Modell beim Hyperparametertuning einen Precision-Wert, der um wenige Prozentpunkte höher ist als derjenige des besten Modells von Chantal. Ansonsten ist die Positionierung der einzelnen Modelle sehr ähnlich, was dazu führte, dass Modelle mit der gleichen Parameterisierung als optimale Recommender ausgewählt wurden.


Fazit

Die Wahl des optimalen Recommendersystems hängt stark von den Anforderungen ab. Bei unserem Beispiel haben wir uns auf Precision und Novelty fokussiert und wählten deswegen das optimierte SVD-Modell. Bei anderen Anforderungen hätten auch der Popular-Recommender oder der item-based Recommender mit der Jaccard-Ähnlichkeit als beste Modelle bestimmt werden können, insbesondere wenn weniger Wert auf Coverage und Novelty gelegt wird. Und bei anderen Daten würden die Resultate wohl auch nicht so aussehen wie bei unserem reduzierten MovieLense-Datensatz. Und schliesslich muss das ausgewählte Modell auch in der Praxis eingesetzt werden können. Insbesondere bei riesigen Mengen an Ratings können gewisse Modelle zu viel Zeit benötigen, um Vorschläge zu liefern. Bei einem user-based Recommender lässt sich beispielsweise die Ähnlichkeitsmatrix nicht im Voraus berechnen und zwischenspeichern, da sie bei jeder neuen Bewertung neu berechnet werden muss.

Die Vergleiche zwischen den beiden Datensets haben gezeigt, dass obwohl nur ca. 30% der Daten übereinstimmen, die Metriken sehr ähnlich sind und auch jeweils die gleichen Modelle gute Resultate liefern. Wo es hingegen grosse Unterschiede gibt, sind die konkreten Filme, die empfohlen werden. Da je nach Modell auch Filme mit nur wenigen Bewertungen zu den am häufigsten empfohlenen Filmen gehören können, sind kleine Änderungen am Datenset für die Kund*innen direkt sichtbar.

Zum Schluss wollen wir uns noch eine Top 10-Liste für eine beliebige Kundin oder einen beliebigen Kunden mit unserem optimalen Recommender generieren lassen.

optimal_recommender(chosen_ratings, 7)

In diesem Fall ist es ein User, dem vor allem Filme aus den Genres Action, Adventure und Comedy empfohlen werden. Und auch ganz viel Drama, Drama.